package com.gitee.jdbc.mongodb.wrappers;

import com.gitee.jdbc.mongodb.JdbcDriver;
import com.gitee.jdbc.mongodb.ScanStrategy;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientException;
import com.mongodb.client.ListDatabasesIterable;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoIterable;
import java.net.URI;
import java.net.URLEncoder;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bson.BsonDocument;
import org.bson.BsonInt64;
import org.bson.Document;
import org.bson.UuidRepresentation;
import org.bson.conversions.Bson;

public class WrappedMongoClient {

  public static final Logger LOGGER = Logger.getLogger(WrappedMongoClient.class.getName());

  private final MongoClient mongoClient;
  private final String databaseName;
  private final String uri;
  private final ScanStrategy scanStrategy;
  public final boolean expandResultSet, sortFields;

  public WrappedMongoClient(String uri, final Properties prop, final String databaseName,
      final ScanStrategy scanStrategy, boolean expandResultSet, boolean sortFields) {
    try {
      String user = prop.getProperty("user");
      String password = prop.getProperty("password");
      if (null != user) {
        URI url = new URI(uri);
        int port = (url.getPort() > 0) ? url.getPort() : 27017;
        String query = (null != url.getQuery() && !url.getQuery().isEmpty()) ? "?" + url.getQuery() : "";
        if (null != password) {
          this.uri = url.getScheme() + "://"
              + URLEncoder.encode(user, "UTF-8") + ":"
              + URLEncoder.encode(password, "UTF-8") + "@"
              + url.getHost() + ":" + port
              + url.getPath() + query;
        } else {
          this.uri = url.getScheme() + "://"
              + URLEncoder.encode(user, "UTF-8") + "@"
              + url.getHost() + ":" + url.getPort()
              + url.getPath() + query;
        }
      } else {
        this.uri = uri;
      }
    } catch (Exception e) {
      throw new MongoClientException("invalid mongo jdbc url", e);
    }

    LOGGER.info("MongoClient URL: " + uri + " rewritten as " + this.uri);

    final ConnectionString connectionString = new ConnectionString(this.uri) {
      @Override
      public Integer getMaxConnectionIdleTime() {
        return Integer.MAX_VALUE;
      }

      @Override
      public UuidRepresentation getUuidRepresentation() {
        return UuidRepresentation.STANDARD;
      }
    };
    this.mongoClient = MongoClients.create(connectionString);
    this.databaseName = databaseName;
    this.expandResultSet = expandResultSet;
    this.scanStrategy = scanStrategy;
    this.sortFields = sortFields;
    getDatabaseNames();
  }

  public boolean pingServer() {
    mongoClient.listDatabaseNames();
    Bson command = new BsonDocument("ping", new BsonInt64(1));
    try {
      mongoClient.getDatabase(databaseName != null && databaseName.length() > 0 ? databaseName : "admin")
          .runCommand(command);
      JdbcDriver.LOGGER.info("Connected successfully to server.");
      return true;
    } catch (Throwable ex) {
    }
    mongoClient.getDatabase("admin").runCommand(command);
    JdbcDriver.LOGGER.info("Connected successfully to server.");
    return true;
  }

  public void close() {
    mongoClient.close();
  }

  public MongoIterable<String> listDatabaseNames() {
    return mongoClient.listDatabaseNames();
  }

  public ListDatabasesIterable<Document> listDatabases() {
    return mongoClient.listDatabases();
  }

  public <T> ListDatabasesIterable<T> listDatabases(Class<T> clazz) {
    return mongoClient.listDatabases(clazz);
  }

  // USE STATIC SO OPENING A NEW CONNECTION WILL REMEMBER THIS
  public static final List<String> createdDatabases = new ArrayList<>();


  public String getCurrentDatabaseName() {
    // SEE THIS TO SEE HOW DATABASE NAME IS USED : http://api.mongodb.org/java/current/com/mongodb/MongoClientURI.html
    return databaseName != null ? databaseName : "admin";
  }

  public List<String> getDatabaseNames() {
    final List<String> names = new ArrayList<>();
    try {
      // THIS OFTEN THROWS EXCEPTION BECAUSE OF MISSING RIGHTS. IN THIS CASE WE ONLY ADD CURRENT KNOWN DB.
      for (String dbName : listDatabaseNames()) {
        names.add(dbName);
      }
    } catch (Throwable ex) {
      names.add(getCurrentDatabaseName());
    }
    for (String str : createdDatabases) {
      if (!names.contains(str)) {
        names.add(str);
      }
    }
    return names;
  }

  private final Map<String, WrappedMongoDatabase> cachedDatabases = new HashMap<>();

  public WrappedMongoDatabase getDatabase(String dbName) {
    if (cachedDatabases.containsKey(dbName)) {
      return cachedDatabases.get(dbName);
    }
    WrappedMongoDatabase db = new WrappedMongoDatabase(mongoClient.getDatabase(dbName), scanStrategy, sortFields);
    cachedDatabases.put(dbName, db);
    return db;
  }

  public List<WrappedMongoDatabase> getDatabases() {
    final List<WrappedMongoDatabase> list = new ArrayList<>();

    for (String dbName : getDatabaseNames()) {
      list.add(getDatabase(dbName));
    }
    return list;
  }


  public String getVersion() {
    return "1.1";
  }

  public String getURI() {
    return uri;
  }


  public List<String> getCollectionNames(String databaseName) throws SQLException {
    final List<String> list = new ArrayList<>();
    try {
      WrappedMongoDatabase db = getDatabase(databaseName);
      if (db != null) {
        for (String name : db.listCollectionNames()) {
          list.add(name);
        }
      }
      list.remove("system.indexes");
      list.remove("system.users");
      list.remove("system.views");
      list.remove("system.version");
    } catch (Throwable ex) {
      JdbcDriver.LOGGER.log(Level.SEVERE, "Cannot list collection names for " + databaseName + ". ", ex);
      throw new SQLException(ex);
    }
    return list;
  }

  public List<String> getViewNames(String databaseName) throws SQLException {
    List<String> list = new ArrayList<>();
    try {
      WrappedMongoDatabase db = getDatabase(databaseName);
      if (db != null) {
        for (Document doc : db.listCollections()) {
          if (doc.containsKey("type") && "view".equals(doc.get("type"))) {
            list.add(String.valueOf(doc.get("name")));
          }
        }
      }
    } catch (Throwable ex) {
      JdbcDriver.LOGGER.log(Level.SEVERE, "Cannot list collection names for " + databaseName + ". ", ex);
      throw new SQLException(ex);
    }
    return list;
  }

}
